local redis = require('resty.redis')
local utils = require('modules.commhm.utils')
local timer = require("modules.commhm.ngxTimerWrap" )
local log   = require('mis_log')
local mis_common = require('mis_common');

local ngx = ngx
local pairs = pairs
local debug = debug
local assert = assert
local unpack = unpack
local string = string
local tonumber = tonumber
local curr = ngx.time
local os_date = os.date
local math_max = math.max
local math_modf = math.modf
local ngx_shared = ngx.shared
local setmetatable = setmetatable

redis.add_commands('zrevrange')

local LAST_FLUSH_TIME = 'LAST_FLUSH_TIME'

local get_last_flush_key = function (work)
    return LAST_FLUSH_TIME .."_" ..  work
end

-- 觉醒活动
local module = string.match(debug.getinfo(1).short_src, '.*/(.*).lua') -- 模块名称
local shared_dict_msg = 'lua_shared_dict ' .. module .. 'undefined'
local this = {
    manager = {},                       -- 实例池
    module  = module,                   -- 模块名称
    profile = module .. '_profile',     -- 角色库
    OrderBy = {
        ASC = 1,                        -- 升序，越小排名越高
        DES = 2,                        -- 降序，越大排名越高
    },
    Updates = {
        FullVolume  = 1,                -- 全量更新
        Incremental = 2,                -- 增量更新
    },
    pool_size        = 100,
    max_idle_timeout = 60000,
    TIME_BASE = 2147483648,

    -- 缓存
    cache = assert(ngx_shared[module], shared_dict_msg),
}

function this:log( t_msg, t_data )
    log.day(self.module, utils.to_str({
        worker = ngx.worker.id(),
        work   = self.work,
        msg    = t_msg,
        data   = t_data
    }))
end

function this:redis( )
    ngx.ctx.redis = ngx.ctx.redis or {}
    if not ngx.ctx.redis[module] then
        -- 新建redis客户端链接
        local rdb = redis:new()
        local ok, err = rdb:connect(self.ip, self.port)
        if not ok then
            self:log('redis:connect',{
                error  = err,
                ip     = self.ip,
                port   = self.port,
            })
            return nil, 'redis connect fault'
        end

        if 0 == rdb:get_reused_times() then
            ok, err = rdb:auth(self.auth)
            if not ok then
                rdb:set_keepalive(this.max_idle_timeout, this.pool_size)
                self:log('redis:auth',{
                    error  = err,
                    ip     = self.ip,
                    port   = self.port,
                    auth   = self.auth
                })
                return nil, 'redis auth fault'
            end
        end

        ngx.ctx.redis[module] = rdb
    end

    return ngx.ctx.redis[module]
end

function this:get_rank( uid )
    local res, err, rdb

    rdb, err = self:redis()
    if not rdb then
        return false, err
    end

    res, err = rdb:zrank(self.rank, uid)
    if err then
        return false, err
    end

    return res
end

function this:update_profile ( uid, profile, check_exists )
    -- 不在榜单内的不需要写入redis
    if check_exists then
        local top, err = self:get_rank(uid)
        if err then
            return false, err
        end

        if not top then return true end
    end

    local res, err, rdb
    rdb, err = self:redis()
    if not rdb then
        return false, err
    end

    res, err = rdb:hset(this.profile, uid, utils.table2json(profile))
    if err then
        self:log('redis:hset', {
            key     = this.profile,
            profile = profile,
            uid     = uid,
            error   = err,
            res     = res,
        })
        return false
    end
    return true
end

function this:update( uid, value, profile )
    local res, err, rdb
    rdb, err  = self:redis()
    if not rdb then
        return false, err
    end

    -- 全量更新
    if self.updates == self.Updates.FullVolume then
        res, err = rdb:zadd(self.rank, value, uid)
        if err then -- res : 新增成员个数
            self:log('redis:zadd',{
                rank  = self.rank,
                uid   = uid,
                value = value,
                error = err,
                res   = res,
            })
            return false, 'redis:zadd fault'
        end

    -- 增量更新
    elseif self.updates == self.Updates.Incremental then
        res, err = rdb:zincrby(self.rank, value, uid)
        if err then -- res 更新之后的value
            self:log('redis:zincrby', {
                rank  = self.rank,
                uid   = uid,
                value = value,
                error = err,
                res   = res,
            })
            return false, 'redis:zincrby fault'
        end
    end

    -- 更新玩家详情
    self:update_profile(uid, profile)
    return true
end

function this:del_from_rank( uin )
    local res, err, rdb
    rdb, err  = self:redis()
    if not rdb then
        return false, err
    end

    res, err = rdb:zrem(self.rank, uin)
    if err then
        self:log("redis:zrem", {
            rank = self.rank,
            uin = uin,
        })
        return false, "redis:zrem fault"
    end

    return true, res
end

function this:flush_redis_key(rdb)
    local now = utils.now()
    local last_key = get_last_flush_key(self.work)
    local data, err = rdb:get(last_key)
    if type(data) ~="string" then
        data, err  = rdb:set(last_key, now)
    else
        local last_time = tonumber(data)
        if not mis_common.is_same_day(last_time, now) then
            data, err  = rdb:set(last_key, now)
            data, err  = rdb:del(self.rank)
            this.log("flush daily rank", {
                redis_key = self.rank,
            })
        end
    end
end


function this:getTop( )
    local rdb, err  = self:redis()
    if not rdb then
        return nil, err
    end
    if  self.daily == 1 then    -- 每日刷新
        self:flush_redis_key(rdb)
    end

    -- 升序排行榜
    local uuidTops, rankTops, res = {}, {}, nil
    if self.order_by == self.OrderBy.ASC then
        res, err = rdb:zrange(self.rank, 0, self.show_num - 1, 'WITHSCORES')
        if err then
            self:log('redis:zrange withscore fail', {
                error = err,
                start = 0,
                final = self.show_num - 1,
            })
            return nil, 'redis:zrange fault'
        end

    -- 降序排行榜
    else
        res, err = rdb:zrevrange(self.rank, 0, self.show_num - 1, 'WITHSCORES')
        if err then
            self:log('redis:zrevrange withscore fail', {
                error = err,
                start = 0,
                final = self.show_num - 1,
            })
            return nil, 'redis:zrevrange fault'
        end
    end
    if utils.tableIsEmpty(res or {}) then
        return {}
    end

    -- 获取玩家数据
    local profileList = {}
    for top = 1, #res / 2, 1 do
        local uid   = res[ 2 * top - 1]
        local value = res[ 2 * top]
        uuidTops[top] =   uid
        rankTops[top] = { uid = uid, value = value }
    end
    profileList, err = rdb:hmget(this.profile, unpack(uuidTops or {}))
    if err then
        self:log('redis:hmget', {
            key    = this.profile,
            fields = uuidTops,
            error  = err
        })
        return nil, 'redis:hmget fault'
    end

    -- 组装排行榜数据
    local result = { rank = {}, total = 0, version = curr()}
    for top, info in pairs(rankTops or {}) do
        result.total = result.total + 1
        result.rank[top] = {
            uid     = info.uid,
            value   = info.value,
            profile = utils.json2table(profileList[top] or '')
        }
    end
    return result
end

function this:set_clean_up(rankTop, cleantime)
    local record_time = math_max(cleantime - curr(), 0)
    local expire_time = record_time + 86400 * 7 -- 活动过期后数据保留7天
    local rdb = self:redis()
    if not rdb or 1 ~= rdb:expire(self.rank, expire_time) then
        self:log('set expire timer fault')
        return false
    end

    -- 1. 设置过期定时器
    local ok, err = self.timer:ngx_timer_at(record_time, function ()
        self:tick()
        -- 2. 把最终结果记录下来
        self:log('set expire succ', {
            final_rank  = rankTop,
            expire_time = os_date("%Y%m%d %H:%M", curr() + expire_time),
            record_time = os_date("%Y%m%d %H:%M", curr() + record_time),
        })
    end)
    self:log('set_clean_up', {ok = ok, err = err})
    return ok and true or false
end

function this:tick()
    -- 拉取最新的榜单数据
    local ranking, err, ok
    ranking, err = self:getTop()
    if err then
        self:log('getTop fail', err)
        return
    end
    -- 写入缓存
    local rankTop = utils.table2json(ranking)
    ok, err = self.cache:set(self.rank, rankTop)
    if not ok then
        self:log('shared:set fault', {
            err   = err,
            rank  = self.rank,
            value = rankTop
        })
    end

    -- 设置了失效时间且有数据
    if  self.clean_time and self:set_clean_up(ranking, self.clean_time) then
        self.clean_time = nil
    end
end

function this:init ()
    if ngx.worker.id() ~= 0 then
        return true
    end

    -- 设置定时器
    self.timer = timer:new()
    local ok, err = self.timer:ngx_timer_every(self.interval, self.tick, self)
    if not ok then
        return false, 'timer init fail : ' .. err
    end

    self:log('init succ')
    return true
end

function this:setting(opt)
    -- 显示榜单如果大于100请考虑好是否需要继续优化缓存方式后再使用
    self.show_num = opt and opt.show_num <= 100 and opt.show_num or 100
    self.order_by = opt and opt.order_by or self.OrderBy.DES
    self.updates  = opt and opt.updates  or self.Updates.FullVolume
    self.daily    = opt and tonumber(opt.daily)  -- 0:每日不清理，1每日清理

    -- 数据按照过期时间缓存多一个月
    self.clean_time = tonumber(opt.end_time)
    return self
end

function this:new(work, config, opt)
    local inst = setmetatable({
        work     = work,
        rank     = this.module .. '_' .. work,
        interval = opt and opt.refresh or 300,  -- 5分钟刷新一次榜单缓存
        -- 连接配置一般不走配置热更
        ip       = config.ip,
        port     = config.port,
        auth     = config.auth,
    }, { __index = this }):setting(opt)

    local ok, desc = inst:init()
    if not ok then
        return nil, desc
    end
    return inst
end

-- 获取排行榜实例
-- params: work   string
-- return: obj    table
this.instance = function ( work )
    return this.manager[work]
end

-- desc: 排行榜注册/刷新配置
-- params: work   string
--         config table :  { ip, port, auth }
--         opt    table :  { show_num, order_by, updates, refresh}
-- return: obj    table
--         err    string
this.register = function (work, config, opt)
    local inst, desc = this.manager[work], nil
    if not inst then
        -- 新榜单
        inst, desc = this:new(work, config, opt)
        if not inst then
            return nil, desc
        end
        this.manager[work] = inst
    else
        inst:setting(opt)
    end

    return inst
end

-- 更新数据
-- params: uid   any
--         value number
--         prof  any
-- return: ok    boolean
--         desc  string
function this:Set( uid, value, prof)
    value = tonumber(value)
    if not (uid and prof) then
        return false, 'invalid role'
    end

    -- 只更新玩家详情
    -- TODO: 这个公共数据也要做数据淘汰，不然redis内存越吃越大
    if not value then
        return self:update_profile(uid, prof, true)
    end

    if self.order_by == self.OrderBy.DES then
        -- 降序下应该是时间越往后对应的值越小
        value = value + (self.TIME_BASE - ngx.time()) / self.TIME_BASE
    else
        -- 升序下时间戳不影响实际排名
        value = tonumber(value .. '.' .. ngx.time())
    end

    return self:update(uid, value, prof)
end

-- 获取榜单
-- params: N/A
-- return: rank  table : {[top] = { uin, value, profile = {}}, ...}
function this:Get( )
    local rankString  = self.cache:get(self.rank)
    if not rankString then
        return nil, 'empty'
    end

    -- 榜单数据统一修正后返回
    local rankTable = utils.json2table(rankString)
    for _, rank in pairs(rankTable.rank or {}) do
        rank.value = math_modf(rank.value)
    end

    return rankTable
end

function this:Del( uin_list )
    if utils.tableIsEmpty(uin_list) then
        return false, "uin list empty"
    end

    local succ, info = self:del_from_rank(uin_list)
    if not succ then
        return false, info
    end

    self:log("del rank member", { uin_list = uin_list, del_cnt = info})
    return true, info
end

return this
